package ar.edu.unicen.exa.isistan.archassis.javatoxml;

import harsh.javatoxml.Exceptions.JXMLException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import ar.edu.unicen.exa.isistan.archassis.classreader.CanonicalClassNameResolver;
import ar.edu.unicen.exa.isistan.archassis.classreader.ClassReaderException;
import ar.edu.unicen.exa.isistan.archassis.classreader.IClassReader;
import ar.edu.unicen.exa.isistan.archassis.ia.api.db.JavaClass;
import ar.edu.unicen.exa.isistan.archassis.ia.seed.MetodoSeed;
import ar.edu.unicen.exa.isistan.archassis.ia.seed.Run;
import ar.edu.unicen.exa.isistan.archassis.ia.seed.VariableSeed;

public class XMLClassReader implements IClassReader {
	private DocumentBuilderFactory factory = null;
	private DocumentBuilder builder = null;
	
	public XMLClassReader() {
		factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(false);
		try {
			builder = factory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		
	}
	@Override
	public JavaClass loadFrom(File file) throws ClassReaderException {
		File xmlJavaFile = getXMLFile(file);
		
		if (xmlJavaFile == null) {
			System.err.println("No pude leer el archivo java como xml: " + file.getAbsolutePath());
			return null;
		}
		
		Document document;
		try {
			document = builder.parse(xmlJavaFile);
		} catch (SAXException e) {
			throw new ClassReaderException("Error cargando " + xmlJavaFile.getAbsolutePath(), e);
		} catch (IOException e) {
			throw new ClassReaderException("Error cargando " + xmlJavaFile.getAbsolutePath(), e);
		}
		
		
		String classNodeXPath = "/java-source-program/java-class-file[@name='" + file.getAbsolutePath() + "']";
		
		Node classNode = getNode(document.getDocumentElement(), classNodeXPath);
		
		if (classNode == null) {
			throw new ClassReaderException("Error cargando " + xmlJavaFile.getAbsolutePath());
		}
		
		//java-source-program/java-class-file/package-decl/@name
		String javaClass_belongsToPackage = getNodeValue(classNode, "package-decl/@name");
		String javaClass_name = getNodeValue(classNode, "class/@name");
		if (javaClass_name == null) {
			//TODO es una interface!
			javaClass_name = getNodeValue(classNode, "interface/@name");
		}
		
		List<String> javaClass_rnImports = getNodeValueList(classNode, "import/@module");
		
		CanonicalClassNameResolver canonicalClassNameResolver = new CanonicalClassNameResolver(javaClass_name, javaClass_belongsToPackage, javaClass_rnImports);
		
		String javaClass_ruExtends = getNodeValue(classNode, "class/superclass/@name");
		if (javaClass_ruExtends == null) {
			javaClass_ruExtends = JavaClass.DEFAULT_EXTENDS;
		}
		
		
		//traduzco la clase al nombre absoluto 
		javaClass_ruExtends = canonicalClassNameResolver.resolve(javaClass_ruExtends);
		
		List<String> javaClass_rnImplements = getNodeValueList(classNode, "class/implement/@interface");
		
		javaClass_rnImplements = canonicalClassNameResolver.resolve(javaClass_rnImplements);
		
		JavaClass javaClass = new JavaClass(javaClass_belongsToPackage + "." + javaClass_name, 
				javaClass_ruExtends, 
				javaClass_rnImplements);
		
		List<VariableSeed> javaClass_variables = getVariables(getNodeList(classNode, "class/field"), javaClass, canonicalClassNameResolver);
		javaClass.setVariables(javaClass_variables);
		
		VariableTypeResolver variableTypeResolver = new VariableTypeResolver(javaClass_variables, canonicalClassNameResolver);
		
		NodeList methods = getNodeList(classNode, "class/method");
		List<MetodoSeed> javaClass_metodo = getMetodoSeedList(methods, canonicalClassNameResolver, variableTypeResolver);
		javaClass.setMetodos(javaClass_metodo);
		return javaClass;
	}
	
	private List<MetodoSeed> getMetodoSeedList(NodeList methods, CanonicalClassNameResolver canonicalClassNameResolver, VariableTypeResolver variableTypeResolver) {
		List<MetodoSeed> result = new ArrayList<MetodoSeed>();
		
		for (int i=0; i < methods.getLength(); i++) {
			Node methodNode = methods.item(i);
			MetodoSeed metodoSeed = getMetodoSeed(methodNode, canonicalClassNameResolver, variableTypeResolver);
			result.add(metodoSeed);
		}
		
		return result;
	}
	
	private static MetodoSeed getMetodoSeed(Node methodNode, CanonicalClassNameResolver canonicalClassNameResolver, VariableTypeResolver variableTypeResolver) {

		String name = getAttributeValue(methodNode, "name");
		String methodType = getNodeValue(methodNode, "type/@name");
		
		//Obtengo una copia del resolvedor de tipos de variable, así tengo un resolvedor con las variables de clase
		VariableTypeResolver variableTypeResolverMethod = copyVariableTypeResolver(variableTypeResolver); 
		
		List<String> parametros = new ArrayList<String>();
		NodeList arguments = getNodeList(methodNode, "formal-arguments/formal-argument");
		loadMethodArguments(arguments, parametros, variableTypeResolverMethod);
		
		NodeList localVariables = getElementsByTagName(methodNode, "local-variable");
		loadMethodVariables(localVariables, variableTypeResolverMethod);
		
		NodeList sends = getElementsByTagName(methodNode, "send");
		List<Run> runList = getRunList(sends, variableTypeResolverMethod, name);
		
		String retorna = canonicalClassNameResolver.resolve(methodType);
		MetodoSeed javaMetodo = new MetodoSeed(name, retorna, parametros, runList);
		return javaMetodo;
	}
	
	private static VariableTypeResolver copyVariableTypeResolver(VariableTypeResolver variableTypeResolver) {
		VariableTypeResolver variableTypeResolverMethod = null;
		try {
			// make deep copy of the object of type CloneExp
			variableTypeResolverMethod = (VariableTypeResolver)variableTypeResolver.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return variableTypeResolverMethod;
	}
	private static void loadMethodArguments(NodeList arguments, List<String> parametros, VariableTypeResolver variableTypeResolver) {
		for (int j=0; j < arguments.getLength(); j++) {
			Node argumentNode = arguments.item(j);
			String argumentNode_name = getAttributeValue(argumentNode, "name");
			String argumentNode_type = getNodeValue(argumentNode, "type/@name");
			
			parametros.add(argumentNode_type);
			variableTypeResolver.put(argumentNode_name, argumentNode_type);
		}
	}
	
	private static void loadMethodVariables(NodeList localVariables, VariableTypeResolver variableTypeResolver) {
		for (int j=0; j < localVariables.getLength(); j++) {
			Node localVariableNode = localVariables.item(j);
			String localVariableNode_name = getAttributeValue(localVariableNode, "name");
			String localVariableNode_type = getNodeValue(localVariableNode, "type/@name");
			
			variableTypeResolver.put(localVariableNode_name, localVariableNode_type);
		}
	}
	
	private static List<Run> getRunList(NodeList sends, VariableTypeResolver variableTypeResolver, String nameMetodo) {
		List<Run> result = new ArrayList<Run>();
		
		for (int j=0; j < sends.getLength(); j++) {
			
			Node sendNode = sends.item(j);
			Run ejecuta = getRun(sendNode, variableTypeResolver, nameMetodo);
			
			result.add(ejecuta);
		}
		return result;
	}
	private static String getTarget(Node sendNode, VariableTypeResolver variableTypeResolver) {
		Node targetNode = getNode(sendNode, "target");
		if (targetNode == null) {
			//no hay un "target", se debería usar "this"
			return "this";
		}
		
		//"target/var-ref/@name"
		Node targetNode_var = getNode(targetNode, "var-ref");
		if (targetNode_var != null) {
			//el "target" es una variable
			String varName = getAttributeValue(targetNode_var, "name");
			return variableTypeResolver.get(varName);
		}
		
		Node targetNode_send = getNode(targetNode, "send");
		if (targetNode_send != null) {
			//Cambiar - por su valor
			Run run = getRun(targetNode_send, variableTypeResolver,"-");
			return run.toString();
		}
		
		Node targetNode_super = getNode(targetNode, "super");
		if (targetNode_super != null) {
			return "super";
		}
		//no sé procesarlo
		return "[ups]";
	}
	private static Run getRun(Node sendNode, VariableTypeResolver variableTypeResolver, String nameMetodo) {
		
		//Este es el nombre del método a ser llamado
		String sendNode_message = getAttributeValue(sendNode, "message");
		
		//Este es el objeto que recibe ese llamado
		String sendNode_target = getTarget(sendNode, variableTypeResolver);
		
		//String paquete = variableTypeResolver.get(sendNode_target);
		
		String nombreMetodo = sendNode_message;
		
		Node sendNode_argumentsNode = getNode(sendNode, "arguments");
		NodeList sendNode_argumentsNodes = sendNode_argumentsNode.getChildNodes();
		
		//obtengo la lista de parametros
		List<String> parametros = getArgumentList(sendNode_argumentsNodes, variableTypeResolver);
		
		String nombreMetodoOrigen = nameMetodo;//"para que?";
		Run run = new Run(sendNode_target, nombreMetodo, parametros, nombreMetodoOrigen);
		return run;
	}
	
	
	
	private static List<String> getArgumentList(NodeList argumentsNodes, VariableTypeResolver variableTypeResolver) {
		List<String> param = new ArrayList<String>();
		if (argumentsNodes.getLength() == 0) {
			param.add("void");
			return param;
		}
		
		for (int k=0; k< argumentsNodes.getLength(); k++) {
			Node sendNode_argumentNode = argumentsNodes.item(k);
			String argumentData = getArgumentData(sendNode_argumentNode, variableTypeResolver);
			if (argumentData != null) {
				param.add(argumentData);
			}
		}
		return param;
	}
	
	private static String getArgumentData(Node argumentNode, VariableTypeResolver variableTypeResolver) {
		String nodeName = argumentNode.getNodeName();
		if (nodeName.equals("#text")) {
			return null;
		}
		if (nodeName.equals("var-ref")) {
			return variableTypeResolver.get(getAttributeValue(argumentNode, "name"));
		}
		if (nodeName.equals("literal-boolean")) {
			return "boolean";
		}
		if (nodeName.equals("literal-string")) {
			return "String";
		}
		if (nodeName.equals("literal-number")) {
			return getAttributeValue(argumentNode, "kind");
		}
		if (nodeName.equals("binary-expr")) {
			//TODO tabla para resolver tipos
			return getBinaryExprArgumentData(argumentNode, variableTypeResolver);
		}
		if (nodeName.equals("new")) {
			return getNodeValue(argumentNode, "type/@name");
		}
		if (nodeName.equals("send")) {
			//Cambiar * por su valor
			Run run = getRun(argumentNode, variableTypeResolver, "*");
			return getMethodReturnType(run.toString());
		}
		//TODO ampliar
		return "[" + nodeName + "] No procesado!";
	}
	
	private static String getBinaryExprArgumentData(Node binExpNode, VariableTypeResolver variableTypeResolver) {
		String op = getAttributeValue(binExpNode, "op");
		
		//si tiene un argumento de tipo "literal-string" es un String
		if (hasChildNamed(binExpNode, "literal-string")) {
			return "java.lang.String";
		}
		
		//obtengo la lista de variables usadas
		NodeList varRefList = getNodeList(binExpNode, "var-ref");
		
		//si hay una variable de tipo String, se retorna String
		for (int i=0; i<varRefList.getLength(); i++) {
			Node child = varRefList.item(i);
			String varName = getAttributeValue(child, "name");
			String varType = variableTypeResolver.get(varName);
			if ("java.lang.String".equals(varType)) {
				return "java.lang.String";
			}
		}
		
		if (hasChildNamed(binExpNode, "literal-number")) {
			//TODO armar la tabla para evaluar el tipo resultante
			return getNodeValue(binExpNode, "literal-number/@kind");
		}
		
		return op;
	}
	
	
	private List<VariableSeed> getVariables(NodeList nodeList, JavaClass javaClass, CanonicalClassNameResolver canonicalClassNameResolver) {
		List<VariableSeed> result = new ArrayList<VariableSeed>();
		for (int i=0; i<nodeList.getLength(); i++) {
			Node nodeI = nodeList.item(i);
			
			String javaVariable_name = getAttributeValue(nodeI, "name");
			String javaVariable_visibility = getAttributeValue(nodeI, "visibility");
			String javaVariable_isStatic = getAttributeValue(nodeI, "static");
			String javaVariable_isFinal = getAttributeValue(nodeI, "final");
			String javaVariable_ruType = getNodeValue(nodeI, "type/@name");//getAttributeValue(nodeI.getFirstChild(), "name");
			
			javaVariable_ruType = canonicalClassNameResolver.resolve(javaVariable_ruType);
			
			if (javaClass.getNombre().equals(javaVariable_ruType)) {
				javaVariable_ruType = "THIS";
			}
			
			VariableSeed javaVariable = new VariableSeed(javaVariable_ruType,
					javaVariable_visibility,
					javaVariable_isStatic,
					javaVariable_isFinal,
					javaVariable_name);
			result.add(javaVariable);
		}
		
		return result;
	}

	private static String getAttributeValue(Node node, String attrName) {
		if (node == null) {
			return "";
		}
		if (node.getAttributes() == null) {
			return "";
		}
		if (node.getAttributes().getNamedItem(attrName) == null) {
			return "";
		}
		return node.getAttributes().getNamedItem(attrName).getNodeValue();
	}
	private List<String> getNodeValueList(Node node, String xpath) {
		List<String> result = new ArrayList<String>();
		NodeList nodeList = getNodeList(node, xpath);
		for (int i=0; i<nodeList.getLength(); i++) {
			Node nodeI = nodeList.item(i);
			result.add(nodeI.getNodeValue());
		}
		return result;
	}
	private static NodeList getNodeList(Node node, String xpath) {
		try {
			return XPathAPI.selectNodeList(node, xpath, node);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}
	private static NodeList getElementsByTagName(Node node, String xpath) {
		try {
			return ((Element)node).getElementsByTagName(xpath);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}
	
	private static String getNodeValue(Node node, String xpath) {
		Node childNode = getNode(node, xpath);
		if (childNode != null) {
			return childNode.getNodeValue();
		}
		return null;
	}
	
	private static Node getNode(Node node, String xpath) {
		try {
			return XPathAPI.selectSingleNode(node, xpath);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}
	private static boolean hasChildNamed(Node node, String tagName) {
		NodeList childrens = node.getChildNodes();
		for (int i=0; i<childrens.getLength(); i++) {
			Node child = childrens.item(i);
			String nodeName = child.getNodeName();
			if (nodeName.equals(tagName)) {
				return true;
			}
		}
		return false;
	}

	private static File getXMLFile(File javaFile) {
		String javaFilePath = javaFile.getAbsolutePath();
		File result = new File(javaFilePath + ".xml");
		
		if (result.isFile()) {  //si el xml ya existe, salgo!
			return result;
		}
		
		JavaToXMLBatch batch = new JavaToXMLBatch(result.getAbsolutePath());
		batch.add(javaFilePath, javaFilePath);
		try {
			batch.convert();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (JXMLException e) {
			e.printStackTrace();
		}
		if (result.isFile()) {
			return result;
		}
		return null;
	}
	private static String getMethodReturnType(String absoluteMethod) {
		if ("Throwable.getMessage(void)".equals(absoluteMethod)) {
			return "java.lang.String";
		}
		System.err.println(absoluteMethod);
		return JavaClass.DEFAULT_EXTENDS;
	}

	public static void main(String[] args) {
		IClassReader classReader = new XMLClassReader();
		//File xmlJavaClass = new File("/home/kache/apps/workspaces/workspace-disenio/ArchAssis-GUI/tmp/svn.codespot.com_a_eclipselabs.org_svn-human-diff_trunk/rev_108/ar.edu.unicen.exa.isistan.plugin.command/src/ar/edu/unicen/exa/isistan/plugin/command/handlers/SampleHandler.java");
		File xmlJavaClass = new File("/home/kache/apps/workspaces/workspace-disenio/ArchAssis-GUI/tmp/svn.codespot.com_a_eclipselabs.org_svn-human-diff_trunk/rev_108/ar.edu.unicen.exa.isistan.plugin.multipage/src/ar/edu/unicen/exa/isistan/plugin/multipage/wizards/SampleNewWizard.java");
		try {
			JavaClass javaClass = classReader.loadFrom(xmlJavaClass);
			List<MetodoSeed> metodoSeedList = javaClass.getMetodos();
			for (MetodoSeed metodoSeed : metodoSeedList) {
				System.out.println(metodoSeed.getAbsoluteData());
				for (Run run : metodoSeed.getEjecuta()) {
					System.out.println("\t"+run.toString());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			
		}
		System.out.println("listo");
	}
}
